/*
 * Copyright 2017 ~ 2025 the original author or authors. James Wong <jameswong1376@gmail.com>
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.wl4g.infra.common.graalvm.polyglot;

import static com.wl4g.infra.common.lang.Assert2.isTrueOf;
import static com.wl4g.infra.common.lang.Assert2.notNullOf;
import static com.wl4g.infra.common.lang.Exceptions.getStackTraceAsString;
import static com.wl4g.infra.common.serialize.JacksonUtils.toJSONString;
import static java.lang.String.format;
import static java.util.Objects.nonNull;
import static org.apache.commons.lang3.SystemUtils.JAVA_IO_TMPDIR;
import static org.apache.commons.lang3.SystemUtils.LINE_SEPARATOR;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.FileHandler;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
import java.util.logging.SimpleFormatter;

import javax.annotation.Nullable;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;

import com.google.common.base.Charsets;
import com.wl4g.infra.common.io.FileIOUtils;

import lombok.Getter;

/**
 * {@link JdkLoggingOutputStream}
 * 
 * @author James Wong
 * @version 2023-01-04
 * @since v1.0.0
 */
@Getter
public class JdkLoggingOutputStream extends OutputStream {

    private final String filePattern;
    private final Level level;
    private final int fileMaxSize;
    private final int fileMaxCount;
    private final Logger logger;
    private final boolean isStdErr;
    private final SimpleFormatter formatter;

    public JdkLoggingOutputStream() {
        this(false);
    }

    public JdkLoggingOutputStream(boolean isStdErr) {
        this(null, null, null, null, true, isStdErr, DEFAULT_FORMATTER);
    }

    public JdkLoggingOutputStream(@Nullable String filePattern, @Nullable Level level, @Nullable @Min(1024) Integer fileMaxSize,
            @Nullable @Min(1) Integer fileMaxCount, boolean enableConsole, boolean isStdErr, @NotNull SimpleFormatter formatter) {
        this.formatter = notNullOf(formatter, "formatter");

        this.filePattern = nonNull(filePattern) ? filePattern
                : JAVA_IO_TMPDIR.concat("/").concat(JdkLoggingOutputStream.class.getSimpleName()).concat(".log");

        this.level = nonNull(level) ? level : Level.ALL;

        if (nonNull(fileMaxSize)) {
            isTrueOf(fileMaxSize >= 1024, "fileMaxSize >= 1024");
        }
        if (nonNull(fileMaxCount)) {
            isTrueOf(fileMaxCount >= 1, "fileMaxCount >= 1");
        }

        // Make ensure base directory.
        try {
            FileIOUtils.forceMkdirParent(new File(this.filePattern));
        } catch (IOException e) {
            throw new IllegalStateException(format("Cannot to create log base dir for filePattern: %s", this.filePattern), e);
        }

        // The default by 512MB
        this.fileMaxSize = nonNull(fileMaxSize) ? fileMaxSize : 512 * 1024 * 1024;
        this.fileMaxCount = nonNull(fileMaxCount) ? fileMaxCount : 10;
        try {
            this.logger = Logger.getLogger(JdkLoggingOutputStream.class.getName() + "." + isStdErr);
            this.logger.setUseParentHandlers(false);
            final FileHandler fileHandler = new FileHandler(this.filePattern, this.fileMaxSize, this.fileMaxCount, true);
            fileHandler.setEncoding("UTF-8");
            fileHandler.setLevel(this.level);
            fileHandler.setFormatter(DEFAULT_FORMATTER);
            this.logger.addHandler(fileHandler);
            if (enableConsole) {
                this.logger.addHandler(new Handler() {
                    @Override
                    public void publish(LogRecord record) {
                        System.out.print(DEFAULT_FORMATTER.format(record));
                    }

                    @Override
                    public void flush() {
                    }

                    @Override
                    public void close() throws SecurityException {
                    }
                });
            }
            this.isStdErr = isStdErr;

        } catch (Exception e) {
            throw new IllegalStateException("Failed to initialize logger", e);
        }
    }

    @Override
    public void write(int b) throws IOException {
        throw new UnsupportedOperationException();
    }

    @Override
    public void write(byte[] b, int off, int len) throws IOException {
        final String msg = new String(b, off, len, Charsets.UTF_8);
        if (isStdErr) {
            logger.warning(msg);
        } else {
            logger.info(msg);
        }
    }

    public static final ThreadLocal<DateTimeFormatter> DEFAULT_DATE_FORMATTER_LOCAL = ThreadLocal
            .withInitial(() -> DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS").withZone(ZoneId.systemDefault()));

    public static final SimpleFormatter DEFAULT_FORMATTER = new SimpleFormatter() {
        @Override
        public String format(LogRecord record) {
            final Map<String, Object> json = new LinkedHashMap<>();
            json.put("timestamp", DEFAULT_DATE_FORMATTER_LOCAL.get().format(record.getInstant()));
            json.put("level", record.getLevel().getName());
            json.put("threadId", record.getThreadID());
            json.put("sequence", record.getSequenceNumber());
            json.put("message", record.getMessage());
            json.put("cause", getStackTraceAsString(record.getThrown()));
            return toJSONString(json).concat(LINE_SEPARATOR);
        }
    };

}
